package cn.terenas.tool.excel.extractor;

import cn.terenas.tool.excel.common.constant.Constant;
import cn.terenas.tool.excel.common.dto.data.ExtractedData;
import cn.terenas.tool.excel.common.dto.data.Transfer;
import cn.terenas.tool.excel.common.exception.ExcelException;
import cn.terenas.tool.excel.util.AssertUtil;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.xssf.usermodel.XSSFCell;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.File;
import java.io.FileInputStream;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

@Slf4j
public class Extractor {


    public LinkedList<ExtractedData> extract() {
        log.info(Constant.SEPARATE_LINE);
        log.info(Constant.SEPARATE_LINE);
        log.info(">>> 开始抽取数据...");
        log.info(Constant.SEPARATE_LINE);
        log.info(Constant.SEPARATE_LINE + "\n");

        LinkedList<ExtractedData> extractedDataLinkedList = getDataFiles()
                .stream()
                .filter(file -> file != null
                        && file.getName().contains(".xlsx")
                        && !file.getName().startsWith("~$")
                        && !file.getName().contains("result")
                )
                .sorted(Comparator.comparing(File::getName))
                .collect(Collectors.toList())
                .stream()
                .map(this::extractTransferListWithAndFilename)
                .filter(Objects::nonNull)
                .filter(extractedData ->
                        StringUtils.isNotEmpty(extractedData.getGroupName())
                                && CollectionUtils.isNotEmpty(extractedData.getTransferList())
                )
                .collect(Collectors.toCollection(Lists::newLinkedList));

        log.info(Constant.SEPARATE_LINE);
        log.info(Constant.SEPARATE_LINE);
        log.info(">>> 数据抽取完成...");
        log.info(Constant.SEPARATE_LINE);
        log.info(Constant.SEPARATE_LINE + "\n");

        return extractedDataLinkedList;
    }

    private ArrayList<File> getDataFiles() {
        try {
            return Lists.newArrayList(Objects.requireNonNull(new File("./").listFiles()));
        } catch (Exception e) {
            log.debug(">>> 读取文件错误, e:", e);
            throw new ExcelException("未检测到数据文件!");
        }
    }

    /**
     * Pair<filename, List<Flight>>
     */
    private ExtractedData extractTransferListWithAndFilename(File file) {
        try (FileInputStream fileInputStream = new FileInputStream(file)) {
            log.info(Constant.SEPARATE_LINE);
            log.info(">>> 正在抽取\"{}\"数据...", file.getName());

            List<List<String>> dataGroupList = extractDataGroupList(
                    file.getName(), new XSSFWorkbook(fileInputStream).getSheetAt(0)
            );

            ExtractedData extractedData = new ExtractedData(
                    file.getName().replace(".xlsx", ""),
                    dataGroupList.stream()
                            .map(this::extractTransferListFromDataGroup)
                            .filter(CollectionUtils::isNotEmpty)
                            .flatMap(Collection::stream)
                            .collect(Collectors.toList())
            );

            log.info(">>> \"{}\"数据抽取完成...", file.getName());
            return extractedData;
        } catch (Exception e) {
            log.debug(">>> \"{}\"数据抽取出错, e:", file.getName(), e);
            log.error(">>> \"{}\"数据抽取出错, 本次处理跳过该文件, 请联系开发人员!", file.getName());
            return null;
        }
    }

    private List<Transfer> extractTransferListFromDataGroup(List<String> dataGroup) {
        try {
            String sourceAirportCode = extractSourceAirportCode(dataGroup);

            String entryFlightCode = extractEntryFlightCode(dataGroup);

            return dataGroup.stream()
                    // Start from 1, the first departure flight.
                    // The first line is the JSCY command line,
                    // which contains source airport code and entry flight code.
                    .skip(1)
                    .map(dataRow -> extractTransfer(dataRow, sourceAirportCode, entryFlightCode))
                    .collect(Collectors.toList());
        } catch (Exception e) {
            log.debug(">>> 单组转机数据处理错误, e:", e);
            log.error(
                    ">>> 单组转机数据处理错误, {}, 本次处理跳过该组数据, 错误数据组:{}\n\n",
                    e.getMessage(), JSON.toJSONString(dataGroup)
            );
            return Lists.newArrayList();
        }
    }

    private Transfer extractTransfer(String data, String sourceAirportCode, String entryFlightCode) {
        try {
            List<String> dataSegments = Lists.newArrayList(data.split(" "))
                    .stream()
                    .filter(StringUtils::isNotBlank)
                    .collect(Collectors.toList());

            String departureFlightCode = extractDepartureFlightCode(data);

            String targetAirportCode = extractTargetAirportCode(data);

            List<Integer> ntcProcessNum = extractProcessNum(
                    sourceAirportCode, departureFlightCode, dataSegments.get(dataSegments.size() - 4), data
            );
            List<Integer> tcProcessNum = extractProcessNum(
                    sourceAirportCode, departureFlightCode, dataSegments.get(dataSegments.size() - 3), data
            );

            return new Transfer(
                    sourceAirportCode, targetAirportCode,
                    entryFlightCode, departureFlightCode,
                    ntcProcessNum.get(0), tcProcessNum.get(0),
                    ntcProcessNum.get(1), tcProcessNum.get(1),
                    ntcProcessNum.get(2), tcProcessNum.get(2),
                    ntcProcessNum.get(3), tcProcessNum.get(3),
                    ntcProcessNum.get(4), tcProcessNum.get(4)

            );
        } catch (Exception e) {
            log.debug(">>> 转机数据解析错误, e:", e);
            log.error(
                    ">>> 来源机场:{}, 入港航班:{}, {}, 转机数据解析错误!",
                    sourceAirportCode, entryFlightCode, e.getMessage()
            );
            throw new ExcelException("转机数据解析错误!");
        }
    }

    private String extractSourceAirportCode(List<String> dataGroup) {
        try {
            String sourceAirportCode = dataGroup.get(0).split("/")[2].split(",")[0].trim().toUpperCase();
            AssertUtil.isTrue(validAirportCode(sourceAirportCode), "\"" + sourceAirportCode + "\", 来源机场编码错误!");

            return sourceAirportCode;
        } catch (ExcelException e) {
            log.error(">>> 来源机场编码解析失败, {}.", e.getMessage());
            throw e;
        } catch (Exception e) {
            log.debug(">>> 来源机场编码解析失败, e:", e);
            log.error(">>> 来源机场编码解析失败, 原始数据:\"{}\"", dataGroup.get(0));
            throw new ExcelException("来源机场编码解析失败!");
        }
    }

    private String extractTargetAirportCode(String data) {
        try {
            String targetAirportCode = data.split("/")[1].trim().toUpperCase();
            AssertUtil.isTrue(validAirportCode(targetAirportCode), "\"" + targetAirportCode + "\", 目的机场编码错误!");
            return targetAirportCode;
        } catch (ExcelException e) {
            log.error(">>> 目的机场编码解析失败, {}.", e.getMessage());
            throw e;
        } catch (Exception e) {
            log.debug(">>> 解析目的机场编码失败, e:", e);
            log.error(">>> 解析目的机场编码失败, 原始数据:\"{}\"", data);
            throw new ExcelException("解析目的机场编码失败!");
        }
    }

    private boolean validAirportCode(String airportCode) {
        return StringUtils.isNotBlank(airportCode)
                && (Constant.DOMESTIC_AIRPORT_CODE_SET.contains(airportCode)
                || Constant.INTERNATIONAL_AIRPORT_CODE_SET.contains(airportCode));
    }

    private String extractDepartureFlightCode(String data) {
        try {
            return data.split("/")[0].trim().toUpperCase();
        } catch (Exception e) {
            log.debug(">>> 解析离港航班编码失败, e:", e);
            log.error(">>> 解析离港航班编码失败, 原始数据:\"{}\"", data);
            throw new ExcelException("解析离港航班编码失败!");
        }
    }

    private String extractEntryFlightCode(List<String> dataGroup) {
        try {
            return dataGroup.get(0).replace("JCSY:", "").replace("JCSY ", "").split("/")[0].trim().toUpperCase();
        } catch (Exception e) {
            log.debug(">>> 入港航班编码解析失败, e:", e);
            log.error(">>> 入港航班编码解析失败, 原始数据:\"{}\"", dataGroup.get(0));
            throw new ExcelException("入港航班编码解析失败!");
        }
    }

    private List<Integer> extractProcessNum(String sourceAirportCode, String departureFlightCode,
                                            String dataSegment, String data) {
        try {
            String[] chkNtcSegments = dataSegment.split("/");
            if (chkNtcSegments.length == 2) {
                return extractTwoClassesFlightProcessNum(chkNtcSegments);
            } else if (chkNtcSegments.length == 3) {
                return extractThreeClassesFlightClassProcessNum(chkNtcSegments);
            } else if (chkNtcSegments.length == 4) {
                return extractFourClassesFlightClassProcessNum(chkNtcSegments);
            } else {
                log.error(
                        ">>> 来源机场:{}, 出港航班:{}, 舱位类型错误!"
                                + "不符合两舱位:\"头等舱/经济舱\""
                                + "或三舱位:\"头等舱/商务舱/经济舱\""
                                + "或四舱位:\"头等舱/商务舱/超级经济舱/经济舱\"舱位类型! "
                                + "原始数据:\"{}\"",
                        sourceAirportCode, departureFlightCode, data
                );
                throw new ExcelException("舱位类型错误!");
            }
        } catch (Exception e) {
            log.debug(">>> 处理舱位数据错误, e:", e);
            throw new ExcelException("舱位数据解析失败!");
        }
    }

    private List<Integer> extractTwoClassesFlightProcessNum(String[] chkNtcSegments) {
        try {
            return Lists.newArrayList(
                    Integer.parseInt(chkNtcSegments[0]),
                    0,
                    0,
                    hasChild(chkNtcSegments[1]) ?
                            Integer.parseInt(chkNtcSegments[1].split("\\+")[0])
                            : Integer.parseInt(chkNtcSegments[1]),
                    hasChild(chkNtcSegments[1]) ?
                            Integer.parseInt(chkNtcSegments[1].split("\\+")[1])
                            : 0
            );
        } catch (Exception e) {
            log.debug(">>> 两舱位数据处理错误, e:", e);
            log.error(">>> 两舱位数据处理错误, 请联系管理员!:");
            throw new ExcelException("两舱位数据处理错误!");
        }
    }

    private List<Integer> extractThreeClassesFlightClassProcessNum(String[] chkNtcSegments) {
        try {

            return Lists.newArrayList(
                    Integer.parseInt(chkNtcSegments[0]),
                    Integer.parseInt(chkNtcSegments[1]),
                    0,
                    hasChild(chkNtcSegments[2]) ?
                            Integer.parseInt(chkNtcSegments[2].split("\\+")[0])
                            : Integer.parseInt(chkNtcSegments[2]),
                    hasChild(chkNtcSegments[2]) ?
                            Integer.parseInt(chkNtcSegments[2].split("\\+")[1])
                            : 0
            );
        } catch (Exception e) {
            log.debug(">>> 三舱位数据处理错误, e:", e);
            log.error(">>> 三舱位数据处理错误, 请联系管理员!:");
            throw new ExcelException("三舱位数据处理错误!");
        }
    }

    private List<Integer> extractFourClassesFlightClassProcessNum(String[] chkNtcSegments) {
        try {

            return Lists.newArrayList(
                    Integer.parseInt(chkNtcSegments[0]),
                    Integer.parseInt(chkNtcSegments[1]),
                    Integer.parseInt(chkNtcSegments[2]),
                    hasChild(chkNtcSegments[3]) ?
                            Integer.parseInt(chkNtcSegments[3].split("\\+")[0])
                            : Integer.parseInt(chkNtcSegments[3]),
                    hasChild(chkNtcSegments[3]) ?
                            Integer.parseInt(chkNtcSegments[3].split("\\+")[1])
                            : 0
            );
        } catch (Exception e) {
            log.debug(">>> 四舱位数据处理错误, e:", e);
            log.error(">>> 四舱位数据处理错误, 请联系管理员!:");
            throw new ExcelException("四舱位数据处理错误!");
        }
    }

    private boolean hasChild(String dataSegment) {
        return StringUtils.isNotBlank(dataSegment) && dataSegment.contains("+");
    }

    private List<List<String>> extractDataGroupList(String filename, XSSFSheet sheet) {
        return extractValidDataGroupList(
                filename, extractRawDataGroupList(filename, sheet)
        );
    }

    private List<List<String>> extractValidDataGroupList(String filename, List<List<String>> rowDataGroupList) {
        final AtomicInteger validDataGroupNum = new AtomicInteger();
        final AtomicInteger validDataRowNum = new AtomicInteger();

        List<List<String>> validDataGroupList = rowDataGroupList.stream()
                .filter(dataGroup -> dataGroup.size() > 1)
                .peek(d -> validDataGroupNum.incrementAndGet())
                .peek(d -> validDataRowNum.addAndGet(d.size()))
                .collect(Collectors.toList());

        log.info(">>> \"{}\"共{}组有效转机数据, 共{}条有效转机数据...", filename, validDataGroupNum, validDataRowNum);
        return validDataGroupList;
    }

    private List<List<String>> extractRawDataGroupList(String filename, XSSFSheet sheet) {
        int lastRowIndex = sheet.getLastRowNum();
        log.info(">>> \"{}\"共{}行数据...", filename, lastRowIndex + 1);

        List<List<String>> rowDataGroupList = new ArrayList<>();
        int groupIndex = 0;
        for (int i = 0; i <= lastRowIndex; i++) {
            String rawRowData = extractRawRowData(sheet.getRow(i));

            if (i == 0 && !validTitle(rawRowData)) {
                throw new ExcelException("首行数据格式错误:未检测到\"JCSY\"命令, 请使用原始数据!");
            }

            if (StringUtils.isNotBlank(rawRowData)) {
                if (validTitle(rawRowData)) {
                    rowDataGroupList.add(Lists.newArrayList());
                    rowDataGroupList.get(groupIndex++).add(rawRowData);
                } else if (validData(rawRowData)) {
                    rowDataGroupList.get(groupIndex - 1).add(rawRowData);
                } else {
                    // Skip invalid data row.
                }
            }
        }
        return rowDataGroupList;
    }

    private String extractRawRowData(XSSFRow row) {
        return Optional.ofNullable(row)
                .map(r -> r.getCell(0))
                .map(XSSFCell::getStringCellValue)
                .map(String::toUpperCase)
                .filter(StringUtils::isNotBlank)
                .orElse("");
    }

    private boolean validTitle(String data) {
        return data.contains("JCSY");
    }

    private boolean invalidData(String data) {
        return data.contains("FLT") || data.contains("NIL")
                || data.contains("NEED") || data.contains("TOTAL")
                || containsChinese(data);
    }

    private boolean validData(String data) {
        return !invalidData(data);
    }

    private boolean containsChinese(String data) {
        return CHINESE_PATTERN.matcher(data).matches();
    }

    private static final Pattern CHINESE_PATTERN = Pattern.compile(".*[\\u4e00-\\u9fa5]+.*");

    private Extractor() {
    }

    private static final Extractor INSTANCE = new Extractor();

    public static Extractor getInstance() {
        return INSTANCE;
    }
}
